
#include "gh.h"
#include "GL/glxew.h"
#include <sys/time.h>

static Display *nDisplay = NULL;
static Window nWindow = 0;
static GLXContext nGLCtx = NULL;

static int nFrameCount = 0;
static double nLastTime, nLastSecond;

static int Run(void) {
    PFNGLXCHOOSEFBCONFIGPROC glXChooseFBConfig;
    PFNGLXGETVISUALFROMFBCONFIGPROC glXGetVisualFromFBConfig;
    GLXFBConfig *fbc;
    int fbcount;
    int attrib[] = {
        GLX_RENDER_TYPE, GLX_RGBA_BIT,
        GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
        GLX_DOUBLEBUFFER, True,
        GLX_RED_SIZE, 8,
        GLX_GREEN_SIZE, 8,
        GLX_BLUE_SIZE, 8,
        GLX_DEPTH_SIZE, 24,
        GLX_STENCIL_SIZE, 8,
        None
    };
    XVisualInfo *visinfo;
    XSetWindowAttributes attr;
    XSizeHints *sizehints;
    Atom wmDeleteMessage;
    int run = 1;
    int r;

    nDisplay = XOpenDisplay(NULL);
    if (!nDisplay) {
        GH_ERROR_SET("XOpenDisplay failed.");
        return -1;
    }

    glXChooseFBConfig = (PFNGLXCHOOSEFBCONFIGPROC) glXGetProcAddress((const GLubyte*) "glXChooseFBConfig");
    if (!glXChooseFBConfig) {
        GH_ERROR_SET("glXGetProcAddress(\"glXChooseFBConfig\") failed.");
        return -1;
    }

    fbc = glXChooseFBConfig(nDisplay, DefaultScreen(nDisplay), attrib, &fbcount);
    if (!fbc) {
        GH_ERROR_SET("glXChooseFBConfig failed.");
        return -1;
    }

    glXGetVisualFromFBConfig = (PFNGLXGETVISUALFROMFBCONFIGPROC) glXGetProcAddress((const GLubyte*) "glXGetVisualFromFBConfig");
    if (!glXGetVisualFromFBConfig) {
        GH_ERROR_SET("glXGetProcAddress(\"glXGetVisualFromFBConfig\") failed.");
        return -1;
    }

    visinfo = glXGetVisualFromFBConfig(nDisplay, fbc[0]);
    if (!visinfo) {
        GH_ERROR_SET("glXGetVisualFromFBConfig failed.");
        return -1;
    }

    attr.background_pixel = 0;
    attr.border_pixel = 0;
    attr.colormap = XCreateColormap(nDisplay, RootWindow(nDisplay, DefaultScreen(nDisplay)), visinfo->visual, AllocNone);
    attr.event_mask = StructureNotifyMask | ExposureMask | KeyPressMask | KeyReleaseMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;

    nWindow = XCreateWindow(nDisplay, RootWindow(nDisplay, DefaultScreen(nDisplay)),
            0, 0, GH_APP_VIEWPORT_WIDTH, GH_APP_VIEWPORT_HEIGHT,
            0, visinfo->depth, InputOutput, visinfo->visual,
            CWBackPixel | CWBorderPixel | CWColormap | CWEventMask, &attr);
    if (!nWindow) {
        GH_ERROR_SET("XCreateWindow failed.");
        return -1;
    }

    XStoreName(nDisplay, nWindow, GH_APP_NAME);
    XMapWindow(nDisplay, nWindow);

    sizehints = XAllocSizeHints();
    if (sizehints) {
        sizehints->min_width = sizehints->max_width = GH_APP_VIEWPORT_WIDTH;
        sizehints->min_height = sizehints->max_height = GH_APP_VIEWPORT_HEIGHT;
        sizehints->flags = PMaxSize | PMinSize;
        XSetWMNormalHints(nDisplay, nWindow, sizehints);
        XFree(sizehints);
    }

    wmDeleteMessage = XInternAtom(nDisplay, "WM_DELETE_WINDOW", False);
    XSetWMProtocols(nDisplay, nWindow, &wmDeleteMessage, 1);

    if (!(nGLCtx = glXCreateContext(nDisplay, visinfo, NULL, True))) {
        GH_ERROR_SET("glXCreateContext failed.");
        return -1;
    }

    if (!glXMakeCurrent(nDisplay, nWindow, nGLCtx)) {
        GH_ERROR_SET("glXMakeCurrent failed.");
        return -1;
    }

    if (glewInit() != GLEW_OK) {
        GH_ERROR_SET("glewInit failed.");
        return -1;
    }

    if (glXSwapIntervalEXT) {
        glXSwapIntervalEXT(nDisplay, nWindow, GH_APP_VSYNC);
    }

    if (ghAppBegin() < 0) {
        return -1;
    }

    nLastTime = nLastSecond = ghTimeGetSec();

    while (run) {
        if (XPending(nDisplay)) {
            XEvent evt;
            XNextEvent(nDisplay, &evt);

            switch (evt.type) {
                case ClientMessage:
                    if (evt.xclient.data.l[0] == wmDeleteMessage) {
                        run = 0;
                    }
                    break;

                case ButtonPress:
                    ghAppMouseCallback(evt.xbutton.x, evt.xbutton.y, GH_MOUSE_DOWN);
                    break;

                case ButtonRelease:
                    ghAppMouseCallback(evt.xbutton.x, evt.xbutton.y, GH_MOUSE_UP);
                    break;

                case MotionNotify:
                    ghAppMouseCallback(evt.xmotion.x, evt.xmotion.y, GH_MOUSE_MOVE);
                    break;

                case KeyRelease:
                case KeyPress: {
                    char asciiCode[32];
                    KeySym keySym;

                    XLookupString(&evt.xkey, asciiCode, sizeof(asciiCode), &keySym, NULL);
                    if (asciiCode[0] == 0x1b /* ESC */) {
                        run = 0;
                    }
                }
            }
        } else {
            double now, dt;
            now = ghTimeGetSec();
            dt = now - nLastTime;
            nLastTime = now;

            if (now - nLastSecond > 1.0) {
                char title[128];
                double fps = nFrameCount / (now - nLastSecond);

                sprintf(title, "Low-Level GPU Programming :: %s [fps: %f, ms: %f]",
                    GH_APP_NAME, fps, (now - nLastSecond) / nFrameCount * 1000);
                XStoreName(nDisplay, nWindow, title);

                nLastSecond = now;
                nFrameCount = 0;
            }

            r = ghAppFrame(dt, now);
            glXSwapBuffers(nDisplay, nWindow);
            nFrameCount++;

            if (glGetError() != GL_NO_ERROR) {
                GH_ERROR_SET("OpenGL error detected.");
                return -1;
            }
            if (r < 0) {
                GH_ERROR_SET("ghAppFrame failed.");
                return -1;
            }
            if (r > 0) {
                GH_TRACE("ghAppFrame ended.");
                return 0;
            }
        }
    }

    GH_TRACE("Main loop ended.");
    return 0;
}

double ghTimeGetSec(void) {
    return ghTimeGetMicroSec() / 1000000.0;
}

double ghTimeGetMicroSec(void) {
    static struct timeval tp;
    gettimeofday(&tp, NULL);
    return (tp.tv_sec * 1000000 + tp.tv_usec);
}

int main(int argc, char **argv) {
    int result = 0;

    if (Run() < 0) {
        result = -1;
        fprintf(stdout, "%s\n", ghErrorGetInfo());
    }

    if (ghAppEnd() < 0) {
        result = -1;
        fprintf(stdout, "%s\n", ghErrorGetInfo());
    }

    if (nGLCtx) {
        glXMakeCurrent(nDisplay, None, NULL);
        glXDestroyContext(nDisplay, nGLCtx);
        nGLCtx = NULL;
    }
    if (nWindow) {
        XDestroyWindow(nDisplay, nWindow);
        nWindow = 0;
    }
    if (nDisplay) {
        XCloseDisplay(nDisplay);
        nDisplay = NULL;
    }

    return result;
}
